在打造高效、可靠的現代 Web 服務時,對「State(應用程式狀態)」與共用資源的正確認知,往往比任何框架的 API 還重要。狀態管理如果做得不好,在高併發情況下很快就會暴露出鎖競爭、資源耗盡或資料不一致等問題;反之,一套清楚的設計原則可以讓你的服務在保持可測試性與可維護性的同時,達到高吞吐量與低延遲。
接下來是一個簡單的範例,教大家如何注入與取用 State
use axum::{Router, routing::get, extract::State};
use std::sync::Arc;
#[derive(Clone)]
struct AppState {
greeting: Arc<String>,
}
async fn hello(state: State<AppState>) -> String {
// greeting 是 Arc<String>,用 &* 來取得 &str 的顯示內容
format!("hello, {}", &*state.greeting)
}
#[tokio::main]
async fn main() {
let state = AppState {
greeting: Arc::new("world".to_string()),
};
let app = Router::new().route("/", get(hello)).with_state(state);
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
.await
.unwrap();
axum::serve(listener, app)
.await
.unwrap();
}
Web 伺服器是多執行緒、多協程並發處理請求;因此共享可變狀態必須保證安全(Send + Sync),並避免資料競爭(data race)。
使用 tokio::sync::RwLock 共享可變 HashMap
use axum::{Router, routing::get, extract::State};
use serde::Serialize;
use std::sync::Arc;
use tokio::sync::RwLock;
use std::collections::HashMap;
#[derive(Clone)]
struct AppState {
users: Arc<RwLock<HashMap<i64, String>>>,
}
async fn list_users(State(state): State<AppState>) -> String {
let users = state.users.read().await;
format!("users: {:?}", users)
}
async fn add_user(State(state): State<AppState>) -> &'static str {
let mut users = state.users.write().await;
users.insert(1, "user1".to_string());
"added"
}
#[tokio::main]
async fn main() {
let state = AppState { users: Arc::new(RwLock::new(HashMap::new())) };
let app = Router::new()
.route("/", get(list_users))
.route("/add", get(add_user))
.with_state(state);
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
.await
.unwrap();
axum::serve(listener, app)
.await
.unwrap();
}
為什麼選 tokio::sync::RwLock 而不是 std::sync::RwLock
用 moka 實作快取(先檢查快取,沒有時,才進行資料庫查詢)
Cargo.toml
moka = { version = "0.12", features = ["future"] }
main.rs
use axum::{Router, extract::State, routing::get};
use moka::future::Cache;
use std::sync::Arc;
#[derive(Clone)]
struct AppState {
cache: Arc<Cache<String, String>>,
}
async fn get_value(State(state): State<AppState>) -> String {
let key = "mykey".to_string();
if let Some(v) = state.cache.get(&key).await {
return format!("from cache: {}", v);
}
// 模擬 DB 查詢
let value = "db-value".to_string();
state.cache.insert(key.clone(), value.clone()).await;
format!("from db: {}", value)
}
#[tokio::main]
async fn main() {
let cache = Cache::new(100_000);
let state = AppState { cache: Arc::new(cache) };
let app = Router::new().route("/", get(get_value)).with_state(state);
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
.await
.unwrap();
axum::serve(listener, app)
.await
.unwrap();
}
什麼情況下,採用外部 cache(如 Redis)還是內部 cache(如 moka)?